8.1 函数参数的默认值
8.1.1 ES6之前
1 | function log (x, y) { |
8.1.2 基本用法
说明:ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
- 阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数或文档
- 有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不回导致以前的代码无法运行
注意:参数变量是默认声明的,不能用let
或const
再次声明,否则会报错1
2
3
4
5
6
7function log (x, y = 'World') {
console.log(x, y)
}
log ('Hello')// Hello World
log ('Hello', China)// Hello China
log ('Hello', '')// Hello
8.1.3 与解构赋值默认值结合使用
案例1:只使用解构赋值默认值1
2
3
4
5
6
7
8function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined
案例二:只使用解构赋值默认值1
2
3
4
5
6
7
8
9function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
案例三:双重默认值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23st=>start: 开始
cond1=>condition: 判断第二个参数是不是 undefined
op1=>operation: 参数默认值生效
cond2=>condition: 参数默认值中 method 是否是 undefined
op2=>operation: 解构赋值中 method 默认值生效
op3=>operation: 参数默认值不生效
op4=>operation: method 通过结构赋值获取值
op5=>operation: 使用传入的 method 值
cond4=>condition: 判断是不是能够解构
op7=>operation: 解构失败(报错)
op8=>operation: method最终为 undefined
e=>end: 结束
st->cond1
cond1(yes)->op1
cond1(no)->cond4
cond4(yes)->op4->e
cond4(no)->op7->e
cond2(yes)->op7->e
cond2(no)->op
op1->cond2
cond2(yes)->op2
op2->e
1 | function fetch(url, { method = 'GET' } = {}) { |
案例四:通过对比深入理解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 写法一:解构中提供默认值
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二:默认参数中提供默认值
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
8.1.4 参数默认值的位置
说明:给函数传递参数时,可以通过空位
省略某个参数。但如果函数定义时某个参数有默认值,传递空位
给它会报错。
技巧:因为设置了默认值的参数后面有没设置默认值的参数时,前者将不能通过空位
省略。因此应该将设置了默认值的参数都排在后面。
注意:传入undefined
将触发该参数等于默认值,null
则没有这个效果。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
8.1.5 函数的 length 属性
length属性:
函数预期传入的参数个数
- 某个参数指定默认值以后,
length
就不计入这个参数了 - rest 参数也不会计入
length
- 设置了默认值的参数后面的参数,无论是否设置了默认值,都不再计入
length
1 | /* 参数指定默认值 */ |
8.1.5 作用域
说明:参数和其默认值引起的一些作用域问题分析。
参数的作用域:参数声明的过程也是变量声明的过程,参数的作用域和函数内部是一致的
函数局部作用域 -> 全局作用域
- 参数默认值为其它声明在前面的参数
1 | // 全局变量 x |
- 参数默认值为其它全局变量
1 | let x = 1; |
- 默认参数的变量在函数作用域和全局作用域都没有找到变量
1 | function f(y = x) { |
- 暂时性死区
1 | var x = 1; |
- 参数的默认值是一个函数
作为默认值的匿名函数在函数foo
定义时被定义,JS静态作用域
的特点决定了此时其作用域已经被确定,只能是全局作用域(因为函数定义阶段作用域链中只有全局对象,活动对象在函数调用阶段才会被创建)1
2
3
4
5
6
7
8
9var x = 1;
function foo(x, y = function() {/* 根据静态作用域的特点,这个 x 的作用域在一开始就确定了,x 始终指向全局的那个 x */x = 2; }) {
var x = 3;
y();
// 这个时候访问的 x 是函数作用域中声明的那个 x ,全局的 x 被屏蔽了
console.log(x);
}
foo() // 3
8.1.6 应用
可以指定某一个参数不得省略,如果省略就抛出一个错误
1 | function throwIfMissing() { |
8.2 rest 参数
说明:ES6
引入rest参数,形式为...变量名
,用于获取函数的多余参数
- 相比 arugments ,rest参数的写法更自然也更简洁
- rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量
- 函数的
length
属性,不计入rest参数
用途:这样就不需要使用arguments
对象了
注意:rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错1
2
3
4
5
6
7// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
8.3 扩展运算符
8.3.1 含义
扩展运算符:用于将数组变为参数序列
说明:相当于rest
参数的逆运算
1 | function push(array, ...items) { |
8.3.2 替代数组的 apply 方法
说明:使用apply
方法的一种用途就是让数组中的每个元素分别作为实参传递到方法中,但这个需求使用扩展运算符实现更加直观简洁。
举个栗子:下面说三个例子
Math.max()
Array.prototype.push()
new Date()
1 | /* 案例一:Math.max() */ |
8.3.3 扩展运算符的应用
8.3.3.1 合并数组
1 | // ES5 |
8.3.3.2 与解构赋值结合
说明:可以达到从数组中到获取子数组的目的
注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
1 | // ES5 |
1 | const [first, ...rest] = [1, 2, 3, 4, 5]; |
1 | const [...butLast, last] = [1, 2, 3, 4, 5]; |
8.3.3.3 函数的返回值
说明:JS
函数如果想返回多个值,只能以数组或对象的方式返回。其中数组形式的返回值用扩展运算符处理非常合适。
1 | var dateFields = readDateFields(database); |
8.3.3.4 字符串
说明:扩展运算符可以将字符串转为真正的数组
注意:能够正确处理32
位的 Unicode
字符
正确获取字符串的 length
1
2
3
4
5[...'hello']
// [ "h", "e", "l", "l", "o" ]
'x\uD83D\uDE80y'.length // 4,这种方式,JavaScript会将32位Unicode字符,识别为2个字符
[...'x\uD83D\uDE80y'].length // 3正确 split 字符串
1
2
3
4
5
6
7let str = 'x\uD83D\uDE80y';
str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'
[...str].reverse().join('')
// 'y\uD83D\uDE80x'
8.3.3.5 实现 Iterator 接口的对象
说明:任何Iterator
接口的对象,都可以用扩展运算符转为真正的数组
1 | var nodeList = document.querySelectorAll('div'); |
8.3.3.6 Map和Set结构,Generator 函数
说明:扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如
- Map 结构
- Set 结构
- Generator 函数返回值
1 | /* Map 结构 */ |
8.4 name属性
说明:函数的name
属性,返回该函数的函数名
es5和es6的对比 | es5 | es6 |
---|---|---|
是否加入到标准 | 否 | 是 |
匿名函数赋值给一个变量, name 属性值 | 空字符串 | 变量名 |
具名函数赋值给一个变量,name 属性值 | 函数本来的名字 | 函数本来的名字 |
(new Function).name | anonymous |
anonymous |
bind返回的函数,name属性值 | “bound 函数名” | “bound 函数名” |
1 | /* 匿名函数赋值给一个变量 */ |
8.5 箭头函数
说明:是一种特殊的函数,相比传统的函数
- 表达更加简洁
- 绑定
this
,没有传统函数this
指向变化带来的困扰
常见用途:简化回调函数
8.5.1 基本用法
参数 => 表达式
参数部分 | 说明 |
---|---|
没有参数 | () |
1个参数 | (参数) 或参数 |
>1个参数 | (参数1, 参数2, ...) |
表达式部分 | 结构 | 返回值 |
---|---|---|
单个语句 | 语句 |
表达式本身的运算结果 |
多个语句 | {语句1; 语句2; return 语句3} |
return 的部分 |
直接返回对象 | (对象字面量) |
对象字面量 |
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14/* 没有参数 */
var f = () => 5;// var f = function () { return 5 };
/* 单个参数 */
var f = v => v;
// var f = function (v) {
// return v;
// }
/* 多个参数 */
var sum = (num1, num2) => num1 + num2;
// var sum = function(num1, num2) {
// return num1 + num2;
//};如果箭头函数的代码块部分多于一条语句,就要使用
{}
将它们括起来,并且使用return
语句返回1
2
3
4var sum = (num1, num2) => {
const pi = 3.14;
return (num1 + num2);
}由于
{}
被解释为代码块
,所以如果箭头函数直接返回一个对象,必须在对象外面加上()
1
var getTempItem = id => ({id: id, name: "Temp"});
与
变量解构
结合使用1
2
3
4const full = ({first, last}) => first + ' ' + last;
// function full(person) {
// return person.first + ' ' + person.last;
// }与 rest 参数结合使用
1
2
3
4
5
6
7/**
* 将参数转换为数组
* @param {可变参数}
*/
const numbers = (...nums) => nums;
// 将
8.5.2 使用注意点
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象 - 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用Rest
参数代替 - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数
箭头函数的this
说明:this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
用途:箭头函数可以让this
指向固定化,这种特性很有利于封装回调函数
注意:由于箭头函数没有自己的this
,所以当然也就不能用call()、apply()、bind()
这些方法去改变this
的指向
原理说明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// ES6
function foo() {
setTimeout(() => {
// 箭头函数里面根本没有自己的this,而是引用外层的this
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
改变 this 无效1
2
3
4
5
6
7(function() {
return [
// 箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
8.5.3 嵌套的箭头函数
管道函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* 管道机制:前一个函数的输出是后一个函数的输入 */
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);
/**
* @param {...Function} funcs 需要进行管道运算的一个一个函数
*/
//var pipeline = function (...funcs) {
// /**
// * @val {Number} 初始值
// */
// return function (val) {
// return funcs.reduce(function (a, b) {
// return b(a)
// }, val)
// }
//}
const plus1 = a => a + 1;// 返回增加1后的值
const mult2 = a => a + 2;// 返回增加2后的值
const addThenMult = pipeline(plus1, mult2);
addThenMult(5)// 12
ℷ演算1
2
3
4
5
6// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
8.6 函数绑定
函数绑定运算符:对象::函数名
功能:该运算符会自动将左边的对象,作为上下文环境(即 this 对象),绑定到右边的函数上面,然后返回
- 如果左边(对象)为空,右边是一个
对象的方法
,则等于将该方法绑定到该对象上面 - 某些情况下可以采用链式写法
用途:用来取代call、apply、bind
调用
兼容性:只是一个ES7
的提案,但Babel
转码器已经支持
取代 bind、 apply1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/* 取代 bind */
contextObj::bar
// bar.bind(contextObj)
/* 取代 apply */
contextObj::bar(...arguments)
// bar.apply(contextObj, arguments)
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 判断对象中是否存在某个值
* @param {obj} 对象
* @param {key} 属性名
*/
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
// return hasOwnProperty.call(obj);
}
简写的情形1
2
3
4
5var method = obj::obj.foo;
// var method = ::obj.foo;
let log = ::console.log;
// var log = console.log.bind(console.log)
链式写法1
2
3
4
5
6
7/* 例子1 */
import { map, takeWhile, forEach } from "iterlib"
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
8.7 尾调用优化
8.7.1 什么是尾调用?
答:指函数的最后一步是调用另一个函数
注意:尾调用不一定出现在函数尾部,只要是最后一步操作即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/* 正确的尾调用 */
function f(x){
return g(x);
}
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
/* 容易被误解为尾调用的情形 */
// 情况一:调用函数g之后,还有赋值操作
function f(x){
let y = g(x);
return y;
}
// 情况二:调用后还有加运算
function f(x){
return g(x) + 1;
}
// 情况三:最后还有一个隐含的 return undefined 的操作
function f(x){
g(x);
}
8.7.2 尾调用优化
说明:JS
引擎在处理尾调用函数
的时候会比普通函数
有更好的内存性能
调用帧:函数调用在内存中形成的一个调用记录,保存调用位置和内部变量等信息
调用栈:如果A调用了B,非尾调用会在A的调用帧上方形成一个B的调用帧,依次类推,形成一个调用栈
;尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,只要直接用内层函数的调用帧,取代外层函数的饿调用帧
注意:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
1 | // 不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one |
8.7.3 尾递归
尾递归:尾递归调用自身
说明:因为同时保存成千上百给调用帧,递归容易造成栈溢出
,但尾递归不回,因为只存在一个调用帧
注意:ES6第一次明确规定,所有ECMAScript
的实现,都必需部署尾调用优化
。
8.7.3.1 计算 n 的阶乘
尾调用1
2
3
4
5
6
7
8
9
10
11
12/**
* 递归计算阶乘
* @param {Number} n 几的阶乘
* @param {Number} total 当前一次乘积值
* @return {Number} 返回的值并不使用,也没有意义,只是为了构成尾递归
*/
function factorial (n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1)// 120
非尾调用1
2
3
4functon factorial (n) {
if (n === 1) return 1;
return n * factional(n - 1);
}
8.7.3.2 fibonacci递归算法
尾调用1
2
3
4
5
6
7
8
9
10
11/**
* 递归计算 fibonacii 数列的和
* @param {Number} 在几以下的 fibonacci 数列
* @param {Number} ac1 fibonacii 的第一个值
* @param {Number} ac2 fibonacii 的第二个值
* @return {Number} fibonacii 数列的和
*/
function fibonacci (n, ac1 = 1, ac2 = 1) {
if (n <=1) return ac2;
return fibonacci(n -1, ac2, ac1 + ac2);
}
非尾调用1
2
3
4function fibonacii (n) {
if (n <= 1) return 1;
return fibonacci(n - 1) + gibonacci(n - 2)
}
8.7.4 递归函数的改写
说明:把所有用到的内部变量改写成函数的参数,确保最后一步只调用自身
技巧:解决尾递归
参数不太易读的问题
- 在尾递归函数之外,再提供一个正常形式的函数
- 将函数柯里化(currying),意思是将多参数的函数转换成单参数的形式
- 采用ES6的函数默认值
1 | // 尾递归函数 |
8.7.5 严格模式
说明:ES6
的尾递归优化值在严格模式下开启,正常模式是无效的
原理:因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。严格模式下,尾调用优化发生,函数的调用栈会改写,因此上面两个变量就会失真
- func.arguments:返回调用时函数的参数
- func.caller:返回调用当前函数的那个函数
1 | function restricted() { |
8.7.6 尾递归优化的实现
说明:正常模式下,或者那些不支持该功能的环境中,可以自己实现尾递归优化。这里提供两种方式
- 利用蹦床函数,将递归执行转为循环执行(需要改写尾递归函数本身)
- 真正的自己实现的尾递归优化(不需要改写递归函数本身)
8.7.6.1 利用蹦床函数
1 | // 递归函数 |
8.7.6.2 真正实现
1 | function tco(f) { |
8.8 函数参数的尾逗号
说明:允许函数的最后一个参数有尾逗号
注意:目前数定义和调用时,都不允许有参数的尾逗号
支持:ES7
的一个提案
用途:在定义函数时,如果参数单独占一行,增加参数时,之前最后一个参数后面要加一个逗号,版本管理系统就会显示,添加逗号的那一行也发生了变动。如果之前最后一个参数后面就有,
,就没有这个问题了
1 | function clownsEverywhere( |